function bind(scope, fn) 
{
    return function () 
    {
        fn.apply(scope, arguments);
    };
}

function $(name)
{
  return document.getElementById(name);
}

function rand(min,max)
{
  return Math.floor(min + (Math.random() * (max-min)));
}

function range(min,max,value)
{
  return value >= min && value <= max;
}

function run()
{
  
  
  
  var engine = new Engine();
  engine.addDrawLayer('bg');
  engine.addDrawLayer('fg');
  
  engine.addClickLayer('click');
  //red = engine.addStyle('red',new CanvasStyle(rgb(255,0,0),rgb(0,0,0),1));
  //blue = engine.addColour('blue',0,0,255);
  //engine.drawRect(1,red,0,0,100,100);
  //engine.drawRect(0,blue,0,0,200,200);
   
  var menu = new Menu(engine);
  
  menu.build();
  

      
  return;
}


function init()
{
  try {
    run();
  // Appears OGG only works
  //var object = new Audio('Sounds/theme.mid');
  //var object = new Audio('Sounds/Tetris.mp3');
  //var object = new Audio('Sounds/test.ogg');
  //object.load();
  //object.play();    
    
  } catch(e)
  {
    alert(e);
  }
}

function Point(x,y)
{
  this.x = x;
  this.y = y;
}

function Rot2DMatrix(angle)
{
  this.angle = angle;
  this.radAngle = this.angle * (Math.PI / 180.0);
  this._11 = this._00 = Math.cos(this.radAngle);
  this._01 = Math.sin(this.radAngle);
  this._10 = -this._01;

  this.multiplyPoint = function (point)
  {
    //00  10 \/  X = 00 x X + 10 x Y 
    //01  11 /\  Y = 01 X X + 11 X Y
    return new Point(this._00 * point.x + this._10 * point.y, this._01 * point.x + this._11 * point.y);
  }
}

function TetrisBlocks(engine)
{
  this.engine = engine;
  
  this.sizeX = this.sizeY = 10;
  this.scale = 1;
  
  this.rotationalMatrices = 
  [
    0,
    new Rot2DMatrix(90),
    new Rot2DMatrix(180),
    new Rot2DMatrix(270)
  ];
  
  this.shapes = 
  [
    [new Point(0,0),new Point(0,1),new Point(1,1),new Point(1,0)],     // Square
    [new Point(0,0),new Point(0,-1),new Point(0,1),new Point(0,2)],    // Straight
    [new Point(0,0),new Point(-1,0),new Point(-1,1),new Point(1,0)],   // L
    [new Point(0,0),new Point(-1,0),new Point(-1,-1),new Point(1,0)],  // Reverse L
    [new Point(0,0),new Point(-1,0),new Point(0,-1),new Point(1,-1)],  // S
    [new Point(0,0),new Point(-1,0),new Point(0,1),new Point(1,1)],    // Reverse S
    [new Point(0,0),new Point(-1,0),new Point(1,0),new Point(0,-1)]    // T
  ];
  
  this.colours = 
  [
    [new CanvasStyle(rgb(255,0,0),rgb(0,0,0),1),new CanvasStyle(rgb(255,50,50),rgb(0,0,0),1)],
    [new CanvasStyle(rgb(0,255,0),rgb(0,0,0),1),new CanvasStyle(rgb(50,255,50),rgb(0,0,0),1)],
    [new CanvasStyle(rgb(0,0,255),rgb(0,0,0),1),new CanvasStyle(rgb(50,50,255),rgb(0,0,0),1)],
    [new CanvasStyle(rgb(255,255,0),rgb(0,0,0),1),new CanvasStyle(rgb(255,255,50),rgb(0,0,0),1)],
    [new CanvasStyle(rgb(255,0,255),rgb(0,0,0),1),new CanvasStyle(rgb(255,50,255),rgb(0,0,0),1)],
    [new CanvasStyle(rgb(0,255,255),rgb(0,0,0),1),new CanvasStyle(rgb(50,255,255),rgb(0,0,0),1)]
  ];
  
  this.rotatePoint = function (point,rotation)
  {
    if(rotation != 0)
    {  
      var rotatedPoint = this.rotationalMatrices[rotation].multiplyPoint(point);
      return new Point(Math.floor(rotatedPoint.x + 0.5),Math.floor(rotatedPoint.y + 0.5));
      //return this.rotationalMatrices[rotation].multiplyPoint(point);
    } else
      return point;    
  }
  
  this.getSizeX = function ()
  {
    return this.sizeX * this.scale;
  }
  
  this.getSizeY = function ()
  {
    return this.sizeY * this.scale;
  }
  
  this.randomColour = function ()
  {
    return Math.floor(Math.random() * this.colours.length);
  }
  
  this.drawSingleBlock = function (l,s,x,y)
  {
    this.engine.drawRect(l,this.colours[s][0],x,y,this.getSizeX(),this.getSizeY());
    this.engine.drawRect(l,this.colours[s][1],x + (this.getSizeX() * 0.25),y + (this.getSizeY() * 0.25),this.getSizeX() / 2,this.getSizeY() / 2);
  }
  
  this.drawBlock = function (l,s,type,rot)
  {
    for(var i=0;i<this.shapes[type].length;i++)
    {
      var rotatedPoint = this.rotatePoint(this.shapes[type][i],rot);
      this.drawSingleBlock(l,s,rotatedPoint.x * this.getSizeX(),-rotatedPoint.y * this.getSizeY());
    } 
  }
}

function Block(type,colour,speed,rotation,rotationSpeed,x,y,scale)
{
  this.type = type;
  this.colour = colour;
  this.speed = speed;
  this.rotation = rotation;
  this.rotationSpeed = rotationSpeed;
  this.x = x;
  this.y = y;
  this.scale = scale;  
}

function Menu(engine)
{ 
  this.inheritFrom = TetrisBlocks;
  this.inheritFrom(engine);
  
  this.drawInterval;
  this.logicTimeout;
  
  this.blocks = Array();
  this.assets = Array();
  this.totalAssets = 0;
  
  this.playOver = 0;
  this.testOver = 0;
  
  this.addImage = function (src)
  {
    this.assets.push(new CanvasImage(src,bind(this,this.preloadCallback)));
  }
  
  this.addAudio = function (src)
  {
    this.assets.push(new CanvasAudio(src,bind(this,this.preloadCallback)));
  }  
  
  this.build = function ()
  {
    this.totalAssets = 2;
    this.addImage('Images/title.png');
    this.addImage('Images/title2.png');
    //this.addAudio('Sounds/Tetris.mp3');
      
  }
  
  this.destroy = function ()
  {
    clearInterval(this.drawInterval);
    clearTimeout(this.logicTimeout); 
  }  
  
  this.preloadCallback = function ()
  {
    var total = 0;
    for(var i=0;i<this.assets.length;i++)
    {
      if(this.assets[i].isLoaded())
        total++;
    }
    
    if(total >= this.totalAssets)
      this.start();
  }
  
  this.start = function ()
  {
    this.engine.mouseMoveCallback = bind(this,this.mouseMove);
    //this.assets[1].object.play();
    this.drawInterval = setInterval(bind(this,this.draw),1);
    this.currentTime = new Date().getTime();
    this.logic();
    this.draw();
  }
   
  
  this.draw = function()
  {    
    var newTime = new Date().getTime();
    var elapsed = newTime - this.currentTime;
    this.currentTime = newTime;
    
    
    this.engine.clearRect(0,0,0,640,480);
    
    for(var i=0;i<this.blocks.length;i++)
    {     
      this.blocks[i].y += (elapsed / 1000) * this.blocks[i].speed;
      this.blocks[i].rotation += (elapsed / 1000) * this.blocks[i].rotationSpeed;

      this.engine.save(0);
      
      this.engine.translate(0,this.blocks[i].x,this.blocks[i].y);
      this.engine.rotate(0,this.blocks[i].rotation * (Math.PI / 180.0));
      
      this.scale = this.blocks[i].scale / 100;
      this.drawBlock(0,this.blocks[i].colour,this.blocks[i].type,0);

      this.engine.restore(0);
      
      if(this.blocks[i].y > 550)
      {
        this.blocks.splice(i,1);
        i--;
      }
    }
    
    this.engine.clearGizmos();
    
    this.engine.clearRect(1,0,0,640,480);
        
    this.engine.drawImage(1,this.assets[0].object,100,100,400,100);
    
    this.engine.startGizmo('Classic',bind(this,this.buttonClick),bind(this,this.buttonMoveOver));
    
    this.engine.drawRect(1,this.colours[this.playOver][0],100,300,100,100);
            
    this.engine.stopGizmo();
    
    this.engine.startGizmo('test',bind(this,this.buttonClick),bind(this,this.buttonMoveOver));
    
    this.engine.drawImage(1,this.assets[this.testOver].object,300,300,100,100);
    
    this.engine.stopGizmo();
    
    //this.drawTestPattern();
  }
  
  this.buttonClick = function (x,y,name)
  {
    switch(name)
    {
      case 'Classic':
        this.destroy();
        var classic = new Classic(this.engine);
        classic.build();
        break;
      case 'test':
    
        break;
    }     
  }
  
  this.buttonMoveOver = function (x,y,name)
  {
    switch(name)
    {
      case 'Classic':
        this.playOver = 1;
        break;
      case 'test':
        this.testOver = 1;
        break;
    }     
  }
  
  this.mouseMove = function (x,y)
  {
    this.playOver = 0;
    this.testOver = 0;
  }
  
  this.logic = function()
  {
    this.blocks.push(new Block(
                      rand(0,this.shapes.length),
                      rand(0,this.colours.length),
                      rand(50,100),
                      rand(0,360),
                      rand(-270,270),
                      rand(0,640),
                      -50,
                      rand(25,300)));
    this.logicTimeout = setTimeout(bind(this,this.logic),rand(250,500));
  }
  

    
  this.drawTestPattern = function()
  {
    this.engine.getDrawLayer(1).context.save();
    for(var i=0;i<this.shapes.length;i++)
    {
      this.engine.getDrawLayer(1).context.translate(40,0);
      this.engine.getDrawLayer(1).context.save();
      for(var j=0;j<this.rotationalMatrices.length;j++)
      {
        this.engine.getDrawLayer(1).context.translate(0,40);
        this.drawBlock(1,this.randomColour(),i,j);
      }
      this.engine.getDrawLayer(1).context.restore();
    } 
    this.engine.getDrawLayer(1).context.restore();   
  }
}


function Classic(engine)
{
  this.inheritFrom = TetrisBlocks;
  this.inheritFrom(engine);  
  
  this.drawInterval = null;
  this.logicInterval = null;
  
  this.grid = Array();
  this.gridX = 8;
  this.gridY = 20;
  
  this.movingBlock;
  this.movingBlockColour;
  this.movingBlockRotation;
  this.movingBlockX;
  this.movingBlockY;
      
  this.nextBlock;
  this.nextBlockColour;
  
  this.score = 0;
  this.drawExit = false;

  this.generateNextBlock = function ()
  {
    this.nextBlock = rand(0,this.shapes.length);
    this.nextBlockColour = rand(0,this.colours.length);
  }
  
  this.makeNextBlockCurrent = function ()
  {
    this.movingBlock = this.nextBlock;
    this.movingBlockColour = this.nextBlockColour;
    this.movingBlockRotation = 0;
    this.movingBlockX = Math.floor(this.gridX / 2);
    this.movingBlockY = this.gridY;
    
    this.generateNextBlock();
  }
    
  this.build = function ()
  {
    this.engine.addStyle('transparent_bg',new CanvasStyle(rgb(0,0,0,0.25)));
    this.engine.addStyle('lines',new CanvasStyle(null,rgb(0,0,0),1));
    var fontStyle = new CanvasStyle(rgb(0,0,0),rgb(0,0,0),1);
    fontStyle.fontAdvanced('20pt Arial');    
    this.engine.addStyle('score-font',fontStyle);
    this.engine.addStyle('popup',new CanvasStyle(rgb(255,0,0),rgb(0,0,0),1));
    this.engine.addStyle('button', new CanvasStyle(rgb(0,255,0),rgb(0,0,0),1));
    
    this.start();
  }
  
  this.destroy = function ()
  {
    clearInterval(this.drawInterval);
  }
   
  this.start = function ()
  {
    this.scale = 2;
        
    this.generateNextBlock();
    this.makeNextBlockCurrent();
    this.clearGrid();
    this.drawBackground();    
    //this.buildTestGrid();
    
    window.addEventListener('keydown',bind(this,this.keyPress),true);
    this.drawInterval = setInterval(bind(this,this.draw),1);
    this.logicInterval = setInterval(bind(this,this.logic),1000);
  }
  
  this.stop = function ()
  {
    clearInterval(this.logicInterval);
    window.removeEventListener('keydown',bind(this,this.keyPress),true);     
  }
  
  this.blockColisionTest = function (x,y,rot)
  {
    // Lots of stupid debugging due to 0,0 being top left
    var changedRot = (this.movingBlockRotation + rot) % this.rotationalMatrices.length;
    //var str = '';
    
    var retVal = true;
    
    //this.drawBackground();
    
    for(var i=0;i<this.shapes[this.movingBlock].length;i++)
    {
      var rotatedPoint = this.rotatePoint(this.shapes[this.movingBlock][i],changedRot);
      var pointToTest = new Point(this.movingBlockX + x + rotatedPoint.x,this.movingBlockY + y + rotatedPoint.y);
      
      //str = str + pointToTest.x + ', ' + pointToTest.y + ' = ';
      
      if(!range(0,this.gridX - 1,pointToTest.x))
      {
        retVal = false;
        break;
      }
      
      if(pointToTest.y < 0)
      {
        retVal = false;
        break;
      }
      
      if(pointToTest.y < this.gridY && this.grid[pointToTest.x][pointToTest.y] != null)
      {
        retVal = false;
        break;
      }
      
    }
    
    //this.engine.drawText(0,this.lineStyle,200,200,str);
    
    return retVal;
  }
  
  this.commitBlock = function ()
  {
    for(var i=0;i<this.shapes[this.movingBlock].length;i++)
    {
      var rotatedPoint = this.rotatePoint(this.shapes[this.movingBlock][i],this.movingBlockRotation);
      var pointToCommit = new Point(this.movingBlockX + rotatedPoint.x,this.movingBlockY + rotatedPoint.y);
      
      if(range(0,this.gridX - 1,pointToCommit.x) && range(0,this.gridY - 1,pointToCommit.y))
        this.grid[pointToCommit.x][pointToCommit.y] = this.movingBlockColour;
      else {
        this.stop();
        this.drawExit = true;
        break;
      }
    }    
  }
  
  this.moveRowsDown = function (row)
  {
    for(var i=row;i<this.gridY;i++)
      for(var j=0;j<this.gridX;j++)
      {
        if(i == this.gridY - 1)
          this.grid[j][i] = null;
        else
          this.grid[j][i] = this.grid[j][i+1];
      }
  }
  
  this.clearCompleteRows = function ()
  {
    var total = 0;
    for(var i=0;i<this.gridY;i++)
    {
      var rowComplete = true;
      
      for(var j=0;j<this.gridX;j++)
        if(this.grid[j][i] == null)
        {
          rowComplete = false;
          break;        
        }
        
      if(rowComplete)
      {
        total++;
        this.moveRowsDown(i);
        i--;
      }
    }
    
    this.score += total * total
  }
  
  this.moveBlock = function (x,y,rot)
  {
    if(this.blockColisionTest(x,y,rot))
    {
      this.movingBlockX += x;
      this.movingBlockY += y;
      this.movingBlockRotation = (this.movingBlockRotation + rot) % this.rotationalMatrices.length;
    } else {
      if(y < 0)
      {
        this.commitBlock();
        this.clearCompleteRows();
        this.makeNextBlockCurrent();
      }
    }
  }
  
  this.keyPress = function (e)
  {
    if(this.drawExit)
      return;
    
    switch(e.keyCode)
    {
      case 37:  // Left
        this.moveBlock(-1,0,0);
        break;
      case 39:  // Right
        this.moveBlock(1,0,0);
        break;
      case 40:  // Down
        this.moveBlock(0,-1,0);
        clearInterval(this.logicInterval);
        if(!this.drawExit)
          this.logicInterval = setInterval(bind(this,this.logic),1000); 
        break;
      case 38:  // Up
        this.moveBlock(0,0,1);
        break;
    }
   
    e.preventDefault();
    return false;
  }    
  
  this.clearGrid = function ()
  {
    this.grid = Array();
    for(var x=0;x<this.gridX;x++)
    {
      this.grid[x] = Array();
      for(var y=0;y<this.gridY;y++)
        this.grid[x][y] = null;
    }    
  }
  
  this.buildTestGrid = function ()
  {
    for(var x=0;x<this.gridX;x++)
      for(var y=0;y<this.gridY;y++)
        this.grid[x][y] = rand(0,this.colours.length);       
  }
  
  
  this.drawBackground = function ()
  {
    this.engine.clearRect(0,0,0,640,480);
  }
  
  this.draw = function ()
  {
    this.engine.clearRect(1,0,0,640,480);
    
    //this.drawBackground();
    
    for(var x=0;x<this.gridX + 1;x++)
      this.engine.drawLine(1,this.engine.getStyle('lines'),x*this.getSizeX(),0,x*this.getSizeX(),(this.gridY + 1) * this.getSizeY());
    
    for(var y=0;y<this.gridY + 2;y++)
      this.engine.drawLine(1,this.engine.getStyle('lines'),0,y * this.getSizeY(),this.gridX*this.getSizeX(),y * this.getSizeY());            
    
    var gridSize = new Point(this.getSizeX() * this.gridX,this.getSizeY() * this.gridY);
        
    for(var x=0;x<this.gridX;x++)
      for(var y=0;y<this.gridY;y++)
        if(this.grid[x][y] != null)
          this.drawSingleBlock(1,this.grid[x][y],x * this.getSizeX(),gridSize.y - (y * this.getSizeY()));
          
    // draw moving block
    this.engine.save(1);
    this.engine.translate(1,this.movingBlockX * this.getSizeX(),gridSize.y - (this.movingBlockY * this.getSizeY()));
    this.drawBlock(1,this.movingBlockColour,this.movingBlock,this.movingBlockRotation);
    this.engine.restore(1);

    // Draw Next Block 
    this.engine.save(1);
    this.engine.translate(1,600, 40);
    this.engine.drawText(1,this.engine.getStyle('score-font'),0,60,this.score);
    this.drawBlock(1,this.nextBlockColour,this.nextBlock,0);
    this.engine.restore(1);
    
    if(this.drawExit)
    {
      this.engine.drawRect(1,this.engine.getStyle('transparent_bg'),0,0,640,480);
      this.engine.drawRect(1,this.engine.getStyle('popup'),200,100,200,200);
      this.engine.drawText(1,this.engine.getStyle('score-font'),200,150,'Congrats you');
      this.engine.drawText(1,this.engine.getStyle('score-font'),200,200,'Scored: ' + this.score);
      
      this.engine.clearGizmos();
      this.engine.startGizmo('main menu',bind(this,this.buttonClick));
      
      this.engine.drawRect(1,this.engine.getStyle('button'),220,210,100,50);
      this.engine.drawText(1,this.engine.getStyle('score-font'),225,250,'EXIT');
      
      this.engine.stopGizmo();
    }
    
  }
  
  this.buttonClick = function (x,y,name)
  {
    this.stop();
    this.destroy();
    var menu = new Menu(this.engine);
    menu.build();
  }  
  
  
  this.logic = function ()
  {
    this.moveBlock(0,-1,0);
  }
}
